﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms.Design.Behavior;

namespace System.Windows.Forms.Design;

/// <summary>
///  The FormDocumentDesigner class builds on the DocumentDesigner. It adds shadowing for form properties that need
///  to be shadowed and it also adds logic to properly paint the form's title bar to match the active document window.
/// </summary>
internal class FormDocumentDesigner : DocumentDesigner
{
    private Size _autoScaleBaseSize = Size.Empty;
    private bool _initializing;
    private bool _autoSize;
    private ToolStripAdornerWindowService _toolStripAdornerWindowService;

    /// <summary>
    ///  Shadow the AcceptButton property at design-time so that we can preserve it when the form is rebuilt.
    ///  Otherwise, form.Controls.Clear() will clear it out when we don't want it to.
    /// </summary>
    private IButtonControl AcceptButton
    {
        get => ShadowProperties[nameof(AcceptButton)] as IButtonControl;
        set
        {
            ((Form)Component).AcceptButton = value;
            ShadowProperties[nameof(AcceptButton)] = value;
        }
    }

    /// <summary>
    ///  Shadow the CancelButton property at design-time so that we can preserve it when the form is rebuilt.
    ///  Otherwise, form.Controls.Clear() will clear it out when we don't want it to.
    /// </summary>
    private IButtonControl CancelButton
    {
        get => ShadowProperties[nameof(CancelButton)] as IButtonControl;
        set
        {
            ((Form)Component).CancelButton = value;
            ShadowProperties[nameof(CancelButton)] = value;
        }
    }

    /// <summary>
    ///  Shadowed version of the AutoScaleBaseSize property. We shadow this so that it always persists. Normally
    ///  only properties that differ from the default values at instantiation are persisted, but this should always
    ///  be written. So, we shadow it and add our own ShouldSerialize method.
    /// </summary>
    private Size AutoScaleBaseSize
    {
        get
        {
            // we don't want to get inherited value from a base form that might have been designed in a different
            // DPI so we recalculate the thing instead of getting AutoScaleBaseSize (QFE 2280)
            SizeF real = Form.GetAutoScaleSize(((Form)Component).Font);
            return new Size((int)Math.Round(real.Width), (int)Math.Round(real.Height));
        }
        set
        {
            // We do nothing at design time for this property; we always want to use the calculated value from the component.
            _autoScaleBaseSize = value;
            ShadowProperties[nameof(AutoScaleBaseSize)] = value;
        }
    }

    /// <summary>
    ///  We shadow the AutoSize property at design-time so that the form doesn't grow and shrink as users fiddle
    ///  with autosize related properties.
    /// </summary>
    private bool AutoSize
    {
        get => _autoSize;
        set => _autoSize = value;
    }

    private bool ShouldSerializeAutoScaleBaseSize()
    {
        // Never serialize this unless AutoScale is turned on
        return !_initializing && ((Form)Component).AutoScale && ShadowProperties.Contains(nameof(AutoScaleBaseSize));
    }

    /// <summary>
    ///  Shadow property for the ClientSize property -- this allows us to intercept client size changes and apply
    ///  the new menu height if necessary
    /// </summary>
    private Size ClientSize
    {
        get
        {
            if (_initializing)
            {
                return new Size(-1, -1);
            }
            else
            {
                Size size = new(-1, -1);
                if (Component is Form form)
                {
                    size = form.ClientSize;

                    // Don't report the size decremented by the scroll bars, otherwise, we'll just lose that size
                    // when we run because the form doesn't take that into consideration (it's too early, it hasn't
                    // layed out and doesn't know it needs scrollbars) when sizing.
                    if (form.HorizontalScroll.Visible)
                    {
                        size.Height += SystemInformation.HorizontalScrollBarHeight;
                    }

                    if (form.VerticalScroll.Visible)
                    {
                        size.Width += SystemInformation.VerticalScrollBarWidth;
                    }
                }

                return size;
            }
        }
        set
        {
            GetService<IDesignerHost>();
            ((Form)Component).ClientSize = value;
        }
    }

    /// <summary>
    ///  Shadow property for the IsMDIContainer property on a form.
    /// </summary>
    private bool IsMdiContainer
    {
        get => ((Form)Control).IsMdiContainer;
        set
        {
            if (!value)
            {
                UnhookChildControls(Control);
            }

            ((Form)Control).IsMdiContainer = value;
            if (value)
            {
                HookChildControls(Control);
            }
        }
    }

    /// <summary>
    ///  Opacity property on control. We shadow this property at design time.
    /// </summary>
    private double Opacity
    {
        get => (double)ShadowProperties[nameof(Opacity)];
        set
        {
            if (value is < (double)0.0f or > (double)1.0f)
            {
                throw new ArgumentException(
                    string.Format(
                        SR.InvalidBoundArgument,
                        "value",
                        value.ToString(CultureInfo.CurrentCulture),
                        (0.0f).ToString(CultureInfo.CurrentCulture),
                        (1.0f).ToString(CultureInfo.CurrentCulture)),
                    nameof(value));
            }

            ShadowProperties[nameof(Opacity)] = value;
        }
    }

    /// <summary>
    ///  Overrides the default implementation of ParentControlDesigner SnapLines. Note that if the Padding property
    ///  is not set on our Form - we'll special case this and add default Padding values to our SnapLines. This was
    ///  a usability request specific to the Form itself. Note that a Form only has Padding SnapLines.
    /// </summary>
    public override IList SnapLines
    {
        get
        {
            IList<SnapLine> snapLines = null;
            AddPaddingSnapLines(ref snapLines);
            if (snapLines is null)
            {
                Debug.Fail("why did base.AddPaddingSnapLines return null?");
                snapLines = new List<SnapLine>(4);
            }

            // if the padding has not been set - then we'll auto-add padding to form - this is a Usability request
            if (Control.Padding == Padding.Empty && snapLines is not null)
            {
                int paddingsFound = 0; // used to short-circuit once we find 4 paddings
                for (int i = 0; i < snapLines.Count; i++)
                {
                    // remove previous padding snaplines
                    if (snapLines[i] is SnapLine snapLine && snapLine.Filter is not null && snapLine.Filter.StartsWith(SnapLine.Padding, StringComparison.Ordinal))
                    {
                        if (snapLine.Filter.Equals(SnapLine.PaddingLeft) || snapLine.Filter.Equals(SnapLine.PaddingTop))
                        {
                            snapLine.AdjustOffset(DesignerUtils.s_defaultFormPadding);
                            paddingsFound++;
                        }

                        if (snapLine.Filter.Equals(SnapLine.PaddingRight) || snapLine.Filter.Equals(SnapLine.PaddingBottom))
                        {
                            snapLine.AdjustOffset(-DesignerUtils.s_defaultFormPadding);
                            paddingsFound++;
                        }

                        if (paddingsFound == 4)
                        {
                            break; // we adjusted all of our paddings
                        }
                    }
                }
            }

            return (IList)snapLines;
        }
    }

    private Size Size
    {
        get => Control.Size;
        set
        {
            IComponentChangeService changeService = GetService<IComponentChangeService>();
            PropertyDescriptorCollection props = TypeDescriptor.GetProperties(Component);
            changeService?.OnComponentChanging(Component, props["ClientSize"]);

            Control.Size = value;
            changeService?.OnComponentChanged(Component, props["ClientSize"]);
        }
    }

    /// <summary>
    ///  Accessor method for the showInTaskbar property on control. We shadow this property at design time.
    /// </summary>
    private bool ShowInTaskbar
    {
        get => (bool)ShadowProperties[nameof(ShowInTaskbar)];
        set => ShadowProperties[nameof(ShowInTaskbar)] = value;
    }

    /// <summary>
    ///  Accessor method for the windowState property on control. We shadow this property at design time.
    /// </summary>
    private FormWindowState WindowState
    {
        get => (FormWindowState)ShadowProperties[nameof(WindowState)];
        set => ShadowProperties[nameof(WindowState)] = value;
    }

    private static void ApplyAutoScaling(SizeF baseVar, Form form)
    {
        // We also don't do this if the property is empty. Otherwise we will perform two GetAutoScaleBaseSize calls
        // only to find that they returned the same value.
        if (!baseVar.IsEmpty)
        {
            SizeF newVarF = Form.GetAutoScaleSize(form.Font);
            Size newVar = new((int)Math.Round(newVarF.Width), (int)Math.Round(newVarF.Height));
            // We save a significant amount of time by bailing early if there's no work to be done
            if (baseVar.Equals(newVar))
            {
                return;
            }

            float percY = newVar.Height / ((float)baseVar.Height);
            float percX = newVar.Width / ((float)baseVar.Width);
            form.Scale(percX, percY);
        }
    }

    /// <summary>
    ///  Disposes of this designer.
    /// </summary>
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            IDesignerHost host = GetService<IDesignerHost>();
            Debug.Assert(host is not null, "Must have a designer host on dispose");

            if (host is not null)
            {
                host.LoadComplete -= OnLoadComplete;
                host.Activated -= OnDesignerActivate;
                host.Deactivated -= OnDesignerDeactivate;
            }

            IComponentChangeService cs = (IComponentChangeService)GetService(typeof(IComponentChangeService));
            if (cs is not null)
            {
                cs.ComponentAdded -= OnComponentAdded;
                cs.ComponentRemoved -= OnComponentRemoved;
            }
        }

        base.Dispose(disposing);
    }

    private void EnsureToolStripWindowAdornerService()
    {
        _toolStripAdornerWindowService ??= GetService<ToolStripAdornerWindowService>();
    }

    /// <summary>
    ///  Initializes the designer with the given component. The designer can get the component's site and request
    ///  services from it in this call.
    /// </summary>
    public override void Initialize(IComponent component)
    {
        // We have to shadow the WindowState before we call base.Initialize
        PropertyDescriptor windowStateProp = TypeDescriptor.GetProperties(component.GetType())["WindowState"];
        if (windowStateProp is not null && windowStateProp.PropertyType == typeof(FormWindowState))
        {
            WindowState = (FormWindowState)windowStateProp.GetValue(component);
        }

        _initializing = true;
        base.Initialize(component);
        _initializing = false;
        AutoResizeHandles = true;

        Debug.Assert(component is Form, "FormDocumentDesigner expects its component to be a form.");

        IDesignerHost host = (IDesignerHost)GetService(typeof(IDesignerHost));
        if (host is not null)
        {
            host.LoadComplete += OnLoadComplete;
            host.Activated += OnDesignerActivate;
            host.Deactivated += OnDesignerDeactivate;
        }

        Form form = (Form)Control;
        form.WindowState = FormWindowState.Normal;
        ShadowProperties[nameof(AcceptButton)] = form.AcceptButton;
        ShadowProperties[nameof(CancelButton)] = form.CancelButton;

        // Monitor component/remove add events for our tray
        IComponentChangeService cs = (IComponentChangeService)GetService(typeof(IComponentChangeService));
        if (cs is not null)
        {
            cs.ComponentAdded += OnComponentAdded;
            cs.ComponentRemoved += OnComponentRemoved;
        }
    }

    /// <summary>
    ///  Called when a component is added to the design container. If the component isn't a control, this will
    ///  demand create the component tray and add the component to it.
    /// </summary>
    private void OnComponentAdded(object source, ComponentEventArgs ce)
    {
        if (ce.Component is ToolStrip && _toolStripAdornerWindowService is null && TryGetService(out IDesignerHost _))
        {
            EnsureToolStripWindowAdornerService();
        }
    }

    /// <summary>
    ///  Called when a component is removed from the design container. Here, we check if a menu is being removed
    ///  and handle removing the Form's mainmenu vs. other menus properly.
    /// </summary>
    private void OnComponentRemoved(object source, ComponentEventArgs ce)
    {
        if (ce.Component is ToolStrip && _toolStripAdornerWindowService is not null)
        {
            _toolStripAdornerWindowService = null;
        }

        if (ce.Component is IButtonControl)
        {
            if (ce.Component == ShadowProperties[nameof(AcceptButton)])
            {
                AcceptButton = null;
            }

            if (ce.Component == ShadowProperties[nameof(CancelButton)])
            {
                CancelButton = null;
            }
        }
    }

    // Called when our document becomes active. We paint our form's border the appropriate color here.
    private unsafe void OnDesignerActivate(object source, EventArgs evevent)
    {
        // Paint the form's title bar UI-active
        if (Control is { } control && control.IsHandleCreated)
        {
            PInvokeCore.SendMessage(control, PInvokeCore.WM_NCACTIVATE, (WPARAM)(BOOL)true);
            PInvoke.RedrawWindow(control, lprcUpdate: null, HRGN.Null, REDRAW_WINDOW_FLAGS.RDW_FRAME);
        }
    }

    /// <summary>
    ///  Called by the host when we become inactive. Here we update the title bar of our form so it's the inactive color.
    /// </summary>
    private unsafe void OnDesignerDeactivate(object sender, EventArgs e)
    {
        if (Control is { } control && control.IsHandleCreated)
        {
            PInvokeCore.SendMessage(control, PInvokeCore.WM_NCACTIVATE, (WPARAM)(BOOL)false);
            PInvoke.RedrawWindow(control, lprcUpdate: null, HRGN.Null, REDRAW_WINDOW_FLAGS.RDW_FRAME);
        }
    }

    /// <summary>
    ///  Called when our code loads. Here we connect us as the selection UI handler for ourselves. This is a
    ///  special case because for the top level document, we are our own selection UI handler.
    /// </summary>
    private void OnLoadComplete(object source, EventArgs evevent)
    {
        if (Control is Form form)
        {
            // The form's ClientSize is reported including the ScrollBar's height. We need to account for this in
            // order to display the form with scrollbars correctly.
            int clientWidth = form.ClientSize.Width;
            int clientHeight = form.ClientSize.Height;
            if (form.HorizontalScroll.Visible && form.AutoScroll)
            {
                clientHeight += SystemInformation.HorizontalScrollBarHeight;
            }

            if (form.VerticalScroll.Visible && form.AutoScroll)
            {
                clientWidth += SystemInformation.VerticalScrollBarWidth;
            }

            // ApplyAutoScaling causes WmWindowPosChanging to be called and there we calculate if we need to
            // compensate for a menu being visible we were causing that calculation to fail if we set ClientSize
            // too early. We now do the right thing and check again if we need to compensate for the menu.
            ApplyAutoScaling(_autoScaleBaseSize, form);
            ClientSize = new Size(clientWidth, clientHeight);
            GetService<BehaviorService>()?.SyncSelection();

            form.PerformLayout();
        }
    }

    /// <summary>
    ///  Allows a designer to filter the set of properties the component it is designing
    ///  will expose through the TypeDescriptor object. This method is called immediately before
    ///  its corresponding "Post" method. If you are overriding this method you should
    ///  call the base implementation before you perform your own filtering.
    /// </summary>
    protected override void PreFilterProperties(IDictionary properties)
    {
        PropertyDescriptor prop;
        base.PreFilterProperties(properties);
        // Handle shadowed properties
        string[] shadowProps = ["Opacity", "IsMdiContainer", "Size", "ShowInTaskBar", "WindowState", "AutoSize", "AcceptButton", "CancelButton"];
        Attribute[] empty = [];
        for (int i = 0; i < shadowProps.Length; i++)
        {
            prop = (PropertyDescriptor)properties[shadowProps[i]];
            if (prop is not null)
            {
                properties[shadowProps[i]] = TypeDescriptor.CreateProperty(typeof(FormDocumentDesigner), prop, empty);
            }
        }

        // Mark auto scale base size as serializable again so we can monitor it for backwards compatibility.
        prop = (PropertyDescriptor)properties["AutoScaleBaseSize"];
        if (prop is not null)
        {
            properties["AutoScaleBaseSize"] = TypeDescriptor.CreateProperty(typeof(FormDocumentDesigner), prop, DesignerSerializationVisibilityAttribute.Visible);
        }

        // And set the new default value attribute for client base size, and shadow it as well.
        prop = (PropertyDescriptor)properties["ClientSize"];
        if (prop is not null)
        {
            properties["ClientSize"] = TypeDescriptor.CreateProperty(typeof(FormDocumentDesigner), prop, new DefaultValueAttribute(new Size(-1, -1)));
        }
    }
}
